﻿using System;
using DAL.Engine.Interface;
using DAL.Engine.Domain;

namespace DAL.Engine.Extention
{
    /// <summary>
    /// The compare strategy.
    /// </summary>
    public enum EqualsStrategyType
    {
        ByIdentifier,       // The two objects are equals with the same id.
        ByIdentifierAndType,// The two objects are equals with the same id and object type.
        DeepPropCompare     // The two objects are equals when properties with the same name are equals
    }

    public static class EntityExtention
    {
        /// <summary>
        /// Determines whether the specified TEntity a is equal to the other TEntity b.
        /// </summary>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <typeparam name="TKey">The type used for the entity Id.</typeparam>
        /// <param name="a">Some one entity.</param>
        /// <param name="b">Some other entity.</param>
        /// <param name="equalsStrategyType">The comparison strategy.</param>
        /// <returns>True if the a is equal to the b.</returns>
        public static bool Equals<TEntity, TKey>(this TEntity a, TEntity b, EqualsStrategyType equalsStrategyType)
            where TEntity: IEntity<TKey>
        {            
            switch (equalsStrategyType)
            {
                case EqualsStrategyType.DeepPropCompare:
                    return DeepPropCompare<TEntity, TKey>(a, b);

                case EqualsStrategyType.ByIdentifierAndType:
                    return ByIdentifierAndType<TEntity, TKey>(a, b);

                case EqualsStrategyType.ByIdentifier:
                    return EqualsById<TEntity, TKey>(a, b);
            }

            throw new ArgumentOutOfRangeException("equalsStrategyType", equalsStrategyType, "Unknown type of comparison strategy.");
        }

        #region Equals Strategy

        private static bool EqualsById<TEntity, TKey>(TEntity a, TEntity b)
            where TEntity : IEntity<TKey>
        {
            if (Object.ReferenceEquals(b, a))
            {
                return true;
            }

            if (a == null && b != null || a != null && b == null)
            {
                return false;
            }

            return a.Id.Equals(b.Id);
        }

        public static bool ByIdentifierAndType<TEntity, TKey>(TEntity a, TEntity b)
            where TEntity : IEntity<TKey>
        {
            if (ReferenceEquals(a, b))
                return true;

            if (NotTransient<TEntity, TKey>(a) && NotTransient<TEntity, TKey>(b) && Equals(a.Id, b.Id))
            {
                var aType = a.GetType();
                var bType = b.GetType();

                return bType.IsAssignableFrom(aType) || aType.IsAssignableFrom(bType);
            }

            return false;
        }

        private static bool NotTransient<TEntity, TKey>(TEntity obj)
            where TEntity: IEntity<TKey>
        {
            return obj != null && Equals(obj.Id, default(TKey)) == false;
        }

        private static bool DeepPropCompare<TEntity, TKey>(TEntity a, TEntity b)
            where TEntity : IEntity<TKey>
        {
            if (EntityExtention.EqualsById<TEntity, TKey>(a, b))
            {
                foreach(var prop in a.GetType().GetProperties())
                {
                    if (prop.CanRead)
                    {
                        var aVal = prop.GetValue(a, null);
                        var bVal = prop.GetValue(b, null);
                        if (!Object.Equals(aVal, bVal))
                        {
                            return false;
                        }
                    }
                }
                return true;
            }
            return false;
        }

        #endregion
    }
}
